/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.guicomponents.action;

import java.util.*;
import javax.swing.*;

/**
 * Manager for {@link ISelectionAction}s.<br>
 * Use this manager to populate selection specific {@link JPopupMenu}s.
 * 
 * @author M. Hautle
 */
public class ActionManager {
	/** Singleton. */
	public static final ActionManager ME = new ActionManager();

	/** Map holding the action mappings. The key declares the object type on which the actions were registred. */
	private final Map<Class<?>, List<ActionEntry>> mapping = new HashMap<Class<?>, List<ActionEntry>>();

	/** Map holding the registred actions. The key is the value retrived by {@link ISelectionAction#getActionID()}. */
	private final Map<String, ISelectionAction> actions = new HashMap<String, ISelectionAction>();

	/** Helper for populating popups. */
	private final PopupHelper helper = new PopupHelper();

	/**
	 * Default constructor.
	 */
	private ActionManager() {
	}

	/**
	 * Returns the action with the given id.<br>
	 * The id is the value returned by {@link ISelectionAction#getActionID()}.
	 * 
	 * @param id The action id
	 * @return The action or null
	 */
	public ISelectionAction getAction(String id) {
		return actions.get(id);
	}

	/**
	 * Registers an action so that it can be retrived by {@link #getAction(String)}.<br> {@link #registerAction(Class, String,ISelectionAction)} registers the
	 * action id also.
	 * 
	 * @param action The action to register
	 */
	public void registerAction(ISelectionAction action) {
		actions.put(action.getActionID(), action);
	}

	/**
	 * Registers the given action on the given class type.<br>
	 * The action id will be registred so that you can retrive it using {@link #getAction(String)}.<br>
	 * 
	 * @param <T> The selection type
	 * 
	 * @param c The destination class
	 * @param action The action to register
	 */
	public <T> void registerAction(Class<?> c, ISelectionAction<T> action) {
		registerAction(c, "", action);
	}

	/**
	 * Registers the given action on the given class type.<br>
	 * The action id will be registred so that you can retrive it using {@link #getAction(String)}.<br>
	 * See {@link PopupHelper#addAction(String, Action)} for further information about the location string.
	 * 
	 * @param <T> The selection type
	 * 
	 * @param c The destination class
	 * @param location The location of the action in the popup
	 * @param action The action to register
	 */
	public <T> void registerAction(Class<?> c, String location, ISelectionAction<T> action) {
		List<ActionEntry> list = mapping.get(c);
		if (list == null)
			mapping.put(c, list = new ArrayList<ActionEntry>());
		list.add(new ActionEntry<T>(action, location));
		actions.put(action.getActionID(), action);
	}

	/**
	 * Populates the given menu according to the passed selection.<br>
	 * Attention this method clears first the content of the menu and populates it then.<br>
	 * Before an action is added {@link ISelectionAction#selectionChanged(List)} will be invoked, and the action will be added only if <code>true</code> is
	 * returned.
	 * 
	 * @param <T> The selection type
	 * 
	 * @param selection The selection
	 * @param menu The menu where to add the actions
	 * @return The passed menu
	 */
	public <T> JPopupMenu initPopup(List<T> selection, JPopupMenu menu) {
		helper.prepareAndSetPopup(menu);
		populatePopup(cloneSelection(selection), helper);
		helper.removeEndSeperators();
		helper.cleanUp();
		return menu;
	}

	/**
	 * Populates the given menu according to the passed selection.<br>
	 * Attention it's your responsability to configure the helper before you call this method and to clean it up after the call!<br>
	 * Before an action is added {@link ISelectionAction#selectionChanged(List)} will be invoked, and the action will be added only if <code>true</code> is
	 * returned.<br>
	 * Consider using {@link #initPopup(List, JPopupMenu)}.
	 * 
	 * @param <T> The selection type
	 * 
	 * @param selection The selection
	 * @param helper The popuphelper to use
	 * @return The passed menu
	 */
	public <T> JPopupMenu initPopup(List<T> selection, PopupHelper helper) {
		populatePopup(selection, helper);
		return helper.getPopup();
	}

	/**
	 * Populates the given menu according to the passed selection.<br>
	 * Attention this method clears first the content of the menu and populates it then.<br>
	 * Before an action is added {@link ISelectionAction#selectionChanged(List)} will be invoked, and the action will be added only if <code>true</code> is
	 * returned.
	 * 
	 * @param selection The selected object
	 * @param menu The menu where to add the actions
	 * @return The passed menu
	 */
	public JPopupMenu initPopup(Object selection, JPopupMenu menu) {
		return initPopup(Collections.singletonList(selection), menu);
	}

	/**
	 * Populates the given menu according to the passed selection.<br>
	 * Attention this method clears first the content of the menu and populates it then.<br>
	 * Before an action is added {@link ISelectionAction#selectionChanged(List)} will be invoked, and the action will be added only if <code>true</code> is
	 * returned.
	 * 
	 * @param <T> The selection type
	 * 
	 * @param selection The selection
	 * @param helper The popup helper to use (holds the popup to populate)
	 */
	private <T> void populatePopup(List<T> selection, PopupHelper helper) {
		for (Class<?> c = selection.get(0).getClass(); c != null; c = c.getSuperclass()) {
			appendActions(selection, c, helper);
			appendInterfaceActions(selection, c, helper);
		}
	}

	/**
	 * Creates a new list holding the content of the passed selection list.<br>
	 * This method removes all <code>null</code> values in the cloned list.
	 * 
	 * @param <T> The selection type
	 * 
	 * @param selection The selection
	 * @return A copy of the selection
	 */
	private <T> List<T> cloneSelection(List<T> selection) {
		final List<T> l = new ArrayList<T>();
		for (T o : selection) {
			if (o != null)
				l.add(o);
		}
		return l;
	}

	/**
	 * Adds all actions of the given class (not recursive) to the passed menu.<br>
	 * If a found action return <code>false</code> when {@link ISelectionAction#selectionChanged(List)} gets invoked, then it will not be added to the menu.
	 * 
	 * @param <T> The selection type
	 * 
	 * @param selection The selected object
	 * @param c The concrete class
	 * @param helper The popup helper to use (holds the popup to populate)
	 */
	private <T> void appendActions(List<T> selection, Class<?> c, PopupHelper helper) {
		if (!isTypeValideForAllElements(selection, c))
			return;
		final List<ActionEntry> list = mapping.get(c);
		if (list == null)
			return;
		for (ActionEntry<T> a : list)
			a.addAction(selection, helper);
	}

	/**
	 * Adds all actions for the interfaces implemented by the given class.<br>
	 * This methode is recursive so it processes every implemented interface of the passed class.
	 * 
	 * @param selection The selection
	 * @param c The class to analyze
	 * @param helper The popup helper to use (holds the popup to populate)
	 */
	private void appendInterfaceActions(List<?> selection, Class<?> c, PopupHelper helper) {
		for (Class<?> i : c.getInterfaces()) {
			appendActions(selection, i, helper);
			appendInterfaceActions(selection, i, helper);
		}
	}

	/**
	 * Checks if all elements of the selection list were assignable from the given type.
	 * 
	 * @param selection The selection list
	 * @param type The type to check
	 * @return True if all elements of the given list were assignable from the given type
	 */
	private boolean isTypeValideForAllElements(List<?> selection, Class<?> type) {
		for (Object o : selection) {
			if (!type.isAssignableFrom(o.getClass()))
				return false;
		}
		return true;
	}

	/**
	 * Entry holding an action and it's location.
	 * 
	 * @param <T> The selection type of the action
	 * 
	 * @author M. Hautle
	 */
	private static class ActionEntry<T> {
		/** The action. */
		final ISelectionAction<T> action;

		/** The location where to add the action. */
		final String location;

		/**
		 * Default constructor.
		 * 
		 * @param action The action
		 * @param location The location of the action in the popupmenu
		 */
		protected ActionEntry(ISelectionAction<T> action, String location) {
			this.action = action;
			this.location = location;
		}

		/**
		 * Adds the action using the given popuphelper if it's active for the given selection.
		 * 
		 * @param selection The selection
		 * @param helper The popup helper
		 */
		protected void addAction(List<T> selection, PopupHelper helper) {
			if (action.selectionChanged(selection))
				helper.addAction(location, action);
		}
	}
}
